﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ESRI.ArcGIS.Geometry;

namespace Bes.Swsp.ToolBox
{
  public class StormDrainNetwork
  {
    private ElementCollection<TransitionNode> _transitions;
    private ElementCollection<InletNode> _inlets;
    private ElementCollection<OutletNode> _outlets;
    private Dictionary<CoordinateKey,NodePoint> _nodeCoordinateDictionary;

    public StormDrainNetwork()
    {
      this.Nodes = new ElementCollection<NodePoint>();
      this.Nodes.ElementAdded += new EventHandler<ElementEventArgs<NodePoint>>(_Nodes_ElementAdded);
      this.Nodes.ElementRemoved += new EventHandler<ElementEventArgs<NodePoint>>(_Nodes_ElementRemoved);

      this.Conveyances = new ElementCollection<Conveyance>();
      this.Conveyances.ElementAdded += new EventHandler<ElementEventArgs<Conveyance>>(_Conveyances_ElementAdded);
      this.Conveyances.ElementRemoved += new EventHandler<ElementEventArgs<Conveyance>>(_Conveyances_ElementRemoved);
    }

    /// <summary>
    /// A collection all catchments associated with inlets.
    /// </summary>
    public ICollection<Catchment> Catchments
    {
      get
      {
        List<Catchment> catchments = (
          from inlet in this.Inlets
          where inlet.CatchmentArea != null
          select inlet.CatchmentArea).ToList();

        return catchments.AsReadOnly();
      }
    }

    /// <summary>
    /// A collection of all nodes keyed by node ID.
    /// </summary>
    public ElementCollection<NodePoint> Nodes { get; private set; }

    /// <summary>
    /// A collection of all transition nodes keyed by node ID.
    /// </summary>
    public ElementCollection<TransitionNode> Transitions
    {
      get
      {
        if (_transitions == null)
        {
          _transitions = new ElementCollection<TransitionNode>();
          foreach (NodePoint node in this.Nodes)
          {
            TransitionNode transition = node as TransitionNode;
            if (transition != null)
            {
              _transitions.Add(transition);
            }
          }
          _transitions.SetReadOnly(true);
        }
        return _transitions;
      }
    }

    /// <summary>
    /// A collection of all inlets keyed by node ID.
    /// </summary>
    public ElementCollection<InletNode> Inlets
    {
      get
      {
        if (_inlets == null)
        {
          _inlets = new ElementCollection<InletNode>();
          foreach (NodePoint node in this.Nodes)
          {
            InletNode inlet = node as InletNode;
            if (inlet != null)
            {
              _inlets.Add(inlet);
            }
          }
          _inlets.SetReadOnly(true);
        }
        return _inlets;
      }
    }

    /// <summary>
    /// A collection of all outlets keyed by node ID.
    /// </summary>
    public ElementCollection<OutletNode> Outlets
    {
      get
      {
        if (_outlets == null)
        {
          _outlets = new ElementCollection<OutletNode>();
          foreach (NodePoint node in this.Nodes)
          {
            OutletNode outlet = node as OutletNode;
            if (outlet != null)
            {
              _outlets.Add(outlet);
            }
          }
          _outlets.SetReadOnly(true);
        }
        return _outlets;
      }
    }

    /// <summary>
    /// A collection of all flowlines associated with outlets.
    /// </summary>
    public ICollection<FlowLine> FlowLines
    {
      get
      {
        List<Conveyance> flowlines = new List<Conveyance>(this.Conveyances.Where(conveyance => conveyance is FlowLine));
        return flowlines.ConvertAll(conveyance => (FlowLine)conveyance).AsReadOnly();
      }
    }

    /// <summary>
    /// The collection of conveyances connecting nodes in this network
    /// </summary>
    public ElementCollection<Conveyance> Conveyances { get; private set; }

    /// <summary>
    /// Resets (removes) all volume reduction and effluent concentration parameters on the network
    /// </summary>
    public void ResetDecumulationParameters()
    {
      foreach (NodePoint node in this.Nodes)
      {
        node.VolumeReduction = 0.0;
        node.ResetEffluentConcentration();
      }
    }

    /// <summary>
    /// Sums all local accumulated values from catchments upstream from each node. Will respect any volume reduction and effluent concentrations that are set.
    /// </summary>
    /// <param name="fromLocal">Whether to accumulate values using local inlet inflow from catchments only. If false, will use existing flows/loads as base and apply decumulation parameters.</param>
    public void Accumulate(bool fromLocal = true)
    {
      try
      {
        this.DetectErrors();
        this.ResetTraversedNodes();

        // Reset values
        if (fromLocal)
        {
          foreach (NodePoint node in Nodes)
          {
            node.TotalAccumulatedFlow = 0.0;
            node.TotalAccumulatedLoad = 0.0;
          }
        }

        foreach (NodePoint node in Nodes)
        {
          if (!node.Traversed && !node.IsInLoop)
          {
            NodePoint bottomNode = _FindDownstreamEnd(node);
            if (bottomNode != null)
            {
              _AccumulateValues(bottomNode, fromLocal);
            }
          }
        }
      }
      finally
      {
        this.ResetTraversedNodes();
      }
    }

    /// <summary>
    /// Detects errors on all nodes in the network. Errors can be checked by using the Errors property of each node after calling this method.
    /// </summary>
    public void DetectErrors()
    {
      foreach (NodePoint node in this.Nodes)
      {
        node.ClearErrors();
      }
      foreach (Conveyance conveyance in this.Conveyances)
      {
        conveyance.ClearErrors();
      }
      _FindLoops();
      _FindDivergentFlowPoints();
      _FindOrphans();
    }

    /// <summary>
    /// Detects which conveyances have flow lower than the specified threshold.
    /// </summary>
    /// <param name="threshold">The flow level below which is considered low flow.</param>
    /// <remarks>The threshold is exclusive, which means the level itself is not considered low flow. This method also
    /// detects zero flow which is flagged seperately.</remarks>
    public void DetectLowFlow(double threshold)
    {
      foreach (Conveyance conveyance in this.Conveyances)
      {
        if (conveyance.MinAccumulatedFlow == 0.0)
        {
          conveyance.SetError(NetworkErrors.ZeroFlow);
        }
        else if(conveyance.MinAccumulatedFlow < threshold)
        {
          conveyance.SetError(NetworkErrors.LowFlow);
        }
      }
    }

    private void _FindDivergentFlowPoints()
    {
      // Finds all nodes that have more than one node pointing up to them. This is not allowed.
      HashSet<NodePoint> nodesPointedToFromDownstream = new HashSet<NodePoint>();

      foreach (NodePoint node in this.Nodes)
      {
        foreach (NodePoint upNode in node.UpstreamNodes)
        {
          if (nodesPointedToFromDownstream.Contains(upNode))
          {
            upNode.SetError(NetworkErrors.Divergent);
          }
          else
            nodesPointedToFromDownstream.Add(upNode);
        }
      }
    }

    private void _FindOrphans()
    {
      foreach (NodePoint node in this.Nodes)
      {
        if (node.IsOrphan)
        {
          node.SetError(NetworkErrors.Orphan);
        }
      }
    }

    private void _FindLoops()
    {
      ResetTraversedNodes();

      try
      {
        foreach (NodePoint node in this.Nodes)
        {
          if (!node.Traversed)
          {
            HashSet<NodePoint> thisTrace = new HashSet<NodePoint>();
            _FindLoopDown(node, thisTrace);
          }
        }
      }
      finally
      {
        ResetTraversedNodes();
      }

      foreach (Conveyance conveyance in this.Conveyances)
      {
        if (conveyance.UpstreamNode.IsInLoop && conveyance.DownstreamNode.IsInLoop)
        {
          conveyance.SetError(NetworkErrors.Loop);
        }
      }
    }

    private NodePoint _FindLoopDown(NodePoint node, HashSet<NodePoint> thisTrace)
    {
      // This is a recursive function. This recursion does not branch, it always follows a single chain of nodes downstream,
      // ending either at the end of the network (a "terminal outlet") or when it loops back on itself (or when it reaches a
      // section that has already been traversed in a previous trace).

      // This method returns the point at which a loop is found. If no loop is found, it returns null.

      // Specifically, the four end cases are:
      // 1) Null -- the last downstream node has no further connection. This is not a loop. It's a valid endpoint. Return null.
      // 2) We have passed this node before; it is in thisTrace. We found a loop. Return this node as the start/end of the loop.
      // 3) We have passed this node before from a different starting location. No need to trace further as we have already
      //    tested downstream. Even if this is a loop node it will already be processed. The trace up to this point will not
      //    part of the loop. Return null.
      // 4) Node is disabled. This is allowed and indicates end of trace. Return null.

      // Cases 1) and 2)
      if (node == null || thisTrace.Contains(node))
        return node;

      // Case 3) and 4)
      if (node.Traversed || !node.Enabled)
        return null;

      // Track that we have visited this node during this particular trace.
      thisTrace.Add(node);

      // Mark this node as traversed (persists through all traces initiated by FindLoops())
      node.Traversed = true;

      // Check if we run into a loop downstream from here (recursion).
      NodePoint nextDownNode = null;
      if (node.DownstreamConveyance != null && node.DownstreamConveyance.Enabled)
      {
        nextDownNode = node.DownstreamConveyance.DownstreamNode;
      }

      NodePoint startOfLoop = _FindLoopDown(nextDownNode, thisTrace);

      // This code is executed as the recursion unwinds.
      if (startOfLoop != null)
      {
        // All nodes after the start of the loop are added to the loop nodes (they are part of the loop).
        node.SetError(NetworkErrors.Loop);

        // If we pass back through the start of the loop, any nodes above the loop are not part of a loop themselves.
        // (Nested loops are not possible).
        if (startOfLoop == node)
        {
          startOfLoop = null;
        }
      }

      // This is the return value as we unwind, it is never the end case. If we are in a loop, it passes back the start
      // of the loop. Once we are out of the loop (or if we never were in one), it returns null.
      return startOfLoop;
    }

    private NodePoint _FindDownstreamEnd(NodePoint node)
    {
      HashSet<NodePoint> thisTrace = new HashSet<NodePoint> { node };

      while (node.DownstreamNode != null)
      {
        if (thisTrace.Contains(node.DownstreamNode))
          break;
        node = node.DownstreamNode;
        thisTrace.Add(node);
      }
      return node;
    }

    private void _AccumulateValues(NodePoint node, bool reset)
    {
      try
      {
        // With perfect data, we should never run into a previously traversed node,
        // but we check anyway to make sure we don't run into any erroneous loops.
        if (node.Traversed)
          return;

        node.Traversed = true;

        foreach (NodePoint upNode in node.UpstreamNodes)
        {
          // Base flow is total original flow before decumulation (only relevant when not reseting flow)
          double baseFlow = upNode.TotalAccumulatedFlow;
          double baseLoad = upNode.TotalAccumulatedLoad;

          _AccumulateValues(upNode, reset);

          if (reset)
          {
            node.TotalAccumulatedFlow += upNode.ReleasedFlow;
            node.TotalAccumulatedLoad += upNode.ReleasedLoad;
          }
          else
          {
            node.TotalAccumulatedFlow -= baseFlow - upNode.ReleasedFlow;
            node.TotalAccumulatedLoad -= baseLoad - upNode.ReleasedLoad;
          }
        }

        if (reset)
        {
          InletNode inletNode = node as InletNode;
          if (inletNode != null)
          {
            inletNode.TotalAccumulatedFlow += inletNode.CatchmentArea.LocalFlowAccumulation;
            inletNode.TotalAccumulatedLoad += inletNode.CatchmentArea.LocalLoadAccumulation;
          }
        }
      }
      catch
      {
        Logger.Log(string.Format("StormDrainNetwork._AccumulateValues: Error occurs at {0}.", node));
        throw;
      }
    }

    internal void ResetTraversedNodes()
    {
      foreach (NodePoint node in this.Nodes)
      {
        node.Traversed = false;
      }
    }

    private void _Conveyances_ElementAdded(object sender, ElementEventArgs<Conveyance> e)
    {
      Conveyance conveyance = e.Element;

      if (conveyance.UpstreamNode == null)
      {
        if (conveyance.Line != null)
        {
          conveyance.UpstreamNode = _FindNodeAtPoint(conveyance.Line.FromPoint);
          if (conveyance.UpstreamNode == null)
          {
            conveyance.UpstreamNode = new NodePoint();
            conveyance.UpstreamNode.Point = conveyance.Line.FromPoint;
          }
        }
        else
        {
          conveyance.UpstreamNode = new NodePoint();
        }
      }

      if (conveyance.DownstreamNode == null)
      {
        if (conveyance.Line != null)
        {
          conveyance.DownstreamNode = _FindNodeAtPoint(conveyance.Line.ToPoint);
          if (conveyance.DownstreamNode == null)
          {
            conveyance.DownstreamNode = new NodePoint();
            conveyance.DownstreamNode.Point = conveyance.Line.ToPoint;
          }
        }
        else
        {
          conveyance.DownstreamNode = new NodePoint();
        }
      }

      if (!this.Nodes.Contains(conveyance.UpstreamNode))
        this.Nodes.Add(conveyance.UpstreamNode);
      if (!this.Nodes.Contains(conveyance.DownstreamNode))
        this.Nodes.Add(conveyance.DownstreamNode);

      conveyance.Network = this;
    }

    private NodePoint _FindNodeAtPoint(IPoint point)
    {
      if (_nodeCoordinateDictionary == null)
      {
        _nodeCoordinateDictionary = new Dictionary<CoordinateKey, NodePoint>();

        foreach (NodePoint node in this.Nodes)
        {
          CoordinateKey coords = new CoordinateKey(node.Point.X, node.Point.Y);
          _nodeCoordinateDictionary[coords] = node;
        }
      }

      CoordinateKey pointCoords = new CoordinateKey(point.X, point.Y);
      if (_nodeCoordinateDictionary.ContainsKey(pointCoords))
        return _nodeCoordinateDictionary[pointCoords];

      return null;
    }

    private void _Conveyances_ElementRemoved(object sender, ElementEventArgs<Conveyance> e)
    {
      if (e != null && e.Element != null)
      {
        // Remove from network
        e.Element.Network = null;

        // Remove connections (including reciprocal)
        if (e.Element.DownstreamNode != null)
        {
          e.Element.DownstreamNode.UpstreamConveyances.Remove(e.Element);
          e.Element.DownstreamNode = null;
        }

        if (e.Element.UpstreamNode != null)
        {
          if (e.Element.UpstreamNode.DownstreamConveyance == e.Element)
            e.Element.UpstreamNode.DownstreamConveyance = null;
          e.Element.UpstreamNode = null;
        }
      }
    }

    private void _Nodes_ElementAdded(object sender, ElementEventArgs<NodePoint> e)
    {
      e.Element.Network = this;
      if(e.Element is TransitionNode)
        _ClearCachedCollections();
      
      if (_nodeCoordinateDictionary != null)
      {
        CoordinateKey coords = new CoordinateKey(e.Element.Point.X, e.Element.Point.Y);
        _nodeCoordinateDictionary[coords] = e.Element;
      }
    }

    private void _Nodes_ElementRemoved(object sender, ElementEventArgs<NodePoint> e)
    {
      if (e != null && e.Element != null)
      {
        // Remove from network
        e.Element.Network = null;
        if(e.Element is TransitionNode)
          _ClearCachedCollections();

        // Remove connections (including reciprocal)
        foreach (Conveyance conveyance in e.Element.UpstreamConveyances)
        {
          if (conveyance.DownstreamNode == e.Element)
          {
            conveyance.DownstreamNode = new NodePoint();
            conveyance.DownstreamNode.Point = conveyance.Line.ToPoint;
          }
        }
        e.Element.UpstreamConveyances.Clear();

        if (e.Element.DownstreamConveyance != null)
        {
          if (e.Element.DownstreamConveyance.UpstreamNode == e.Element)
          {
            e.Element.DownstreamConveyance.UpstreamNode = new NodePoint();
            e.Element.DownstreamConveyance.UpstreamNode.Point = e.Element.DownstreamConveyance.Line.FromPoint;
          }
          e.Element.DownstreamConveyance = null;
        }

        // Remove from coordinate dictionary
        if (_nodeCoordinateDictionary != null)
        {
          CoordinateKey coords = new CoordinateKey(e.Element.Point.X, e.Element.Point.Y);
          _nodeCoordinateDictionary.Remove(coords);
        }
      }
    }

    private void _ClearCachedCollections()
    {
      _transitions = null;
      _inlets = null;
      _outlets = null;
    }
  }
}
